package com.wmada.toolkit.common;

import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author will.ma
 * @date 2018-5-29
 */
@SuppressWarnings({"WeakerAccess", "unused", "MagicConstant"})
public class TimeUtils {

    public static final int  YEAR_IN_MONTHS         = 12;
    public static final int  WEEK_IN_DAYS           = 7;
    public static final int  MONTH_IN_DAYS          = 30;
    public static final int  YEAR_IN_DAYS           = 365;
    public static final int  DAY_IN_HOURS           = 24;
    public static final int  YEAR_IN_HOURS          = DAY_IN_HOURS * 365;
    public static final int  MONTH_IN_HOURS         = DAY_IN_HOURS * 30;
    public static final int  WEEK_IN_HOURS          = DAY_IN_HOURS * 7;
    public static final int  HOUR_IN_MINUTES        = 60;
    public static final int  DAY_IN_MINUTES         = 1440;
    public static final int  YEAR_IN_MINUTES        = DAY_IN_MINUTES * 365;
    public static final int  MONTH_IN_MINUTES       = DAY_IN_MINUTES * 30;
    public static final int  WEEK_IN_MINUTES        = DAY_IN_MINUTES * 7;
    /**
     * The constant MINUTE_IN_SECONDS.
     */
    public static final int  MINUTE_IN_SECONDS      = 60;
    /**
     * The constant HOUR_IN_SECONDS.
     */
    public static final int  HOUR_IN_SECONDS        = 3600;
    /**
     * The constant DAY_IN_SECONDS.
     */
    public static final int  DAY_IN_SECONDS         = 86400;
    /**
     * The constant YEAR_IN_SECONDS.
     */
    public static final int  YEAR_IN_SECONDS        = DAY_IN_SECONDS * 365;
    /**
     * The constant MONTH_IN_SECONDS.
     */
    public static final int  MONTH_IN_SECONDS       = DAY_IN_SECONDS * 30;
    public static final int  WEEK_IN_SECONDS        = DAY_IN_SECONDS * 7;
    /**
     * The constant SECOND_IN_MILLISECONDS.
     */
    public static final long SECOND_IN_MILLISECONDS = 1000;
    /**
     * The constant MINUTE_IN_MILLISECONDS.
     */
    public static final long MINUTE_IN_MILLISECONDS = 60000;
    /**
     * The constant HOUR_IN_MILLISECONDS.
     */
    public static final long HOUR_IN_MILLISECONDS   = 3600000;
    /**
     * The constant DAY_IN_MILLISECONDS.
     */
    public static final long DAY_IN_MILLISECONDS    = 86400000;
    /**
     * The constant YEAR_IN_MILLISECONDS.
     */
    public static final long YEAR_IN_MILLISECONDS   = DAY_IN_MILLISECONDS * 365;
    /**
     * The constant MONTH_IN_MILLISECONDS.
     */
    public static final long MONTH_IN_MILLISECONDS  = DAY_IN_MILLISECONDS * 30;
    public static final long WEEK_IN_MILLISECONDS   = DAY_IN_MILLISECONDS * 7;

    private static final String  EXP_REGEX   = "^([+\\-]?)(\\d+)(\\w+)$";
    private static final Pattern EXP_PATTERN = Pattern.compile(EXP_REGEX, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

    private static final String  INT_REGEX   = "^[+\\-]?\\d+$";
    private static final Pattern INT_PATTERN = Pattern.compile(INT_REGEX, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

    private static final List<DateFormat> POSSIBILITIES = new LinkedList<>(Arrays.asList(DateFormat.DATETIME_FORMAT, DateFormat.DATE_FORMAT));

    /**
     * Gets now.
     *
     * @return the now
     */
    public static Date now() {
        return Calendar.getInstance(Locale.getDefault()).getTime();
    }

    private static Calendar current() {
        return Calendar.getInstance(Locale.getDefault());
    }

    public static Date parse(String exp) {
        if (StringUtils.isBlank(exp)) {
            return null;
        }
        Date date = parseDate(exp);
        return date != null ? date : parse(current(), exp);
    }

    public static Date parse(Calendar calendar, String exp) {
        exp = exp.replaceAll("!.*", "").trim();
        if (INT_PATTERN.matcher(exp).matches()) {
            calendar.add(Calendar.SECOND, Integer.parseInt(exp));
            return calendar.getTime();
        }
        Matcher matcher = EXP_PATTERN.matcher(exp);
        if (matcher.matches()) {
            boolean plus     = !"-".equals(matcher.group(1));
            int     distance = Integer.parseInt(matcher.group(2)) * (plus ? 1 : -1);
            int     step     = parseStep(matcher.group(3));
            calendar.add(step, distance);
            return calendar.getTime();
        } else {
            return null;
        }
    }

    public static Date parseDate(String str) {
        Date date;
        for (DateFormat possibility : POSSIBILITIES) {
            date = possibility.parse(str, null);
            if (date != null) {
                return date;
            }
        }
        return null;
    }

    public static Long parseInterval(String exp, TimeUnit unit, Long defaultValue) {
        if (StringUtils.isBlank(exp)) {
            return defaultValue;
        }
        try {
            return parseInterval(exp, unit);
        } catch (IllegalArgumentException e) {
            return defaultValue;
        }
    }

    public static Long parseInterval(String exp, TimeUnit unit) {
        exp = exp.replaceAll("!.*", "").trim();
        if (INT_PATTERN.matcher(exp).matches()) {
            return unit.convert(Integer.parseInt(exp), TimeUnit.SECONDS);
        }
        Matcher matcher = EXP_PATTERN.matcher(exp);
        if (matcher.matches()) {
            boolean  plus     = !"-".equals(matcher.group(1));
            int      distance = Integer.parseInt(matcher.group(2));
            TimeUnit timeUnit = parseUnit(matcher.group(3));
            return (plus ? 1 : -1) * unit.convert(distance, timeUnit);
        } else {
            throw new IllegalArgumentException("Unrecognized Expression: " + exp);
        }
    }

    public static Long parseInterval(String exp, Long defaultValue) {
        return parseInterval(exp, TimeUnit.MILLISECONDS, defaultValue);
    }

    public static Long parseInterval(String exp) {
        return parseInterval(exp, TimeUnit.MILLISECONDS);
    }

    private static int parseStep(String exp) {
        switch (exp) {
            case "y":
            case "year":
                return Calendar.YEAR;
            case "mon":
            case "month":
                return Calendar.MONTH;
            case "w":
            case "week":
                return Calendar.WEEK_OF_YEAR;
            case "d":
            case "day":
                return Calendar.DAY_OF_YEAR;
            case "h":
            case "hour":
                return Calendar.HOUR;
            case "m":
            case "min":
            case "minute":
                return Calendar.MINUTE;
            case "s":
            case "sec":
            case "second":
                return Calendar.SECOND;
            case "ms":
            case "millisecond":
                return Calendar.MILLISECOND;
            case "ns":
            case "nanosecond":
                return 0;
            default:
                return 0;
        }
    }

    private static TimeUnit parseUnit(String exp) {
        switch (exp) {
            case "d":
            case "day":
                return TimeUnit.DAYS;
            case "h":
            case "hour":
                return TimeUnit.HOURS;
            case "m":
            case "min":
            case "minute":
                return TimeUnit.MINUTES;
            case "s":
            case "sec":
            case "second":
                return TimeUnit.SECONDS;
            case "ms":
            case "millisecond":
                return TimeUnit.MILLISECONDS;
            case "ns":
            case "nanosecond":
                return TimeUnit.NANOSECONDS;
            default:
                throw new IllegalArgumentException("Unrecognized Expression: " + exp);
        }
    }

}
